"""
多角色编排器
实现 Planner、Outliner、Character Designer、Scriptwriter、Editor 的协作流程
"""
from typing import Dict, Any, List, Optional, Tuple
from loguru import logger

from storyforge.models import (
    RoleRun, OutlinePlan, EpisodeBeats, CharacterCard, 
    ReviewReport, ScenePlan, StoryBible
)
from storyforge.config import get_config, get_role_config, get_provider_config
from storyforge.providers.base import ProviderBase
from storyforge.providers import LLMProviderFactory
from storyforge.utils import (
    generate_id, log_role_run, log_cost,
    create_multi_role_run_dir, save_role_output, save_json, save_text
)


class RoleRouter:
    """角色路由器"""
    
    def __init__(self):
        """初始化路由器"""
        self.config = get_config()
    
    def get_provider_for_role(self, role: str) -> Tuple[str, str]:
        """
        获取角色对应的提供商和模型
        
        Args:
            role: 角色名称
            
        Returns:
            (provider_name, model_name) 元组
        """
        role_config = get_role_config(role)
        if role_config:
            return role_config.provider, role_config.model
        
        # 回退到默认LLM提供商
        default_provider = self.config.defaultProvider.llm
        provider_config = get_provider_config(default_provider)
        model = provider_config.model if provider_config else "gpt-4o-mini"
        
        return default_provider, model
    
    def create_provider(self, provider_name: str) -> ProviderBase:
        """
        创建提供商实例
        
        Args:
            provider_name: 提供商名称
            
        Returns:
            提供商实例
        """
        return LLMProviderFactory.create_provider(provider_name)


class RoleExecutor:
    """角色执行器"""
    
    def __init__(self, router: RoleRouter):
        """
        初始化执行器
        
        Args:
            router: 角色路由器
        """
        self.router = router
    
    def execute_role(
        self,
        role: str,
        inputs: Dict[str, Any],
        story_bible: Optional[StoryBible] = None
    ) -> RoleRun:
        """
        执行角色任务
        
        Args:
            role: 角色名称
            inputs: 输入参数
            story_bible: 故事事实库
            
        Returns:
            RoleRun对象
        """
        provider_name, model_name = self.router.get_provider_for_role(role)
        provider = self.router.create_provider(provider_name)
        
        run_id = generate_id(f"{role}_")
        
        # 构建提示词
        prompt = self._build_prompt(role, inputs, story_bible)
        
        try:
            # 调用LLM生成
            messages = [
                {"role": "system", "content": self._get_role_system_prompt(role)},
                {"role": "user", "content": prompt}
            ]
            
            # 所有角色都要求JSON格式输出
            response_format = {"type": "json_object"}
            
            # 确定温度参数
            config = get_config()
            llm_config = getattr(config, 'llm', None)
            base_temp = llm_config.defaultTemperature if llm_config else 0.7
            
            # 1. 角色特定配置优先
            role_cfg = get_role_config(role)
            if role_cfg and role_cfg.temperature is not None:
                temperature = role_cfg.temperature
            else:
                # 2. 智能推断任务类型
                target_key = "role_playing" # 默认
                if role in ["planner", "outliner"]:
                    target_key = "outline"
                elif role == "scriptwriter":
                    if inputs.get("task") == "novel_writing":
                        target_key = "novel"
                    else:
                        target_key = "script"
                elif role == "editor":
                    target_key = "json_extraction" # 评审需要理性
                
                if llm_config and hasattr(llm_config, 'temperatures') and target_key in llm_config.temperatures:
                    temperature = llm_config.temperatures[target_key]
                else:
                    temperature = base_temp
            
            result = provider.generate(
                messages=messages,
                temperature=temperature,
                response_format=response_format
            )
            
            # 解析结果
            import json
            try:
                output = json.loads(result) if isinstance(result, str) else result

                # 如果output是列表，根据角色与任务包装成字典
                if isinstance(output, list):
                    task = inputs.get("task")
                    if role == "scriptwriter":
                        # 小说写作模式：封装为 chapters；否则为 scenes
                        if task == "novel_writing":
                            output = {"chapters": output}
                        else:
                            output = {"scenes": output, "productionNotes": ""}
                    elif role == "character_designer":
                        output = {"characters": output}
                    else:
                        # 其他角色的列表输出也包装成字典
                        output = {"items": output}

                # 确保output是字典；并做脚本作者在小说模式下的兜底转换
                if not isinstance(output, dict):
                    output = {"result": str(output)}
                else:
                    if role == "scriptwriter" and inputs.get("task") == "novel_writing":
                        # 若缺少 chapters，但存在 scenes，则兼容为 chapters
                        if "chapters" not in output and isinstance(output.get("scenes"), list):
                            output["chapters"] = output.pop("scenes")
                    
            except json.JSONDecodeError as e:
                logger.warning(f"[{role}] JSON解析失败，尝试提取JSON块: {e}")
                logger.warning(f"[{role}] 原始输出（前1000字符）: {result[:1000] if isinstance(result, str) else str(result)[:1000]}")
                # 尝试从文本中提取JSON块
                output = self._extract_json_from_text(result)
                if not output:
                    logger.error(f"[{role}] 无法从文本中提取有效JSON")
                    raise Exception(f"无法解析JSON输出: {e}")
            
            # 估算成本（简化，实际应该从API响应中获取）
            cost = self._estimate_cost(provider_name, len(str(result)))
            
            role_run = RoleRun(
                id=run_id,
                role=role,
                inputs=inputs,
                output=output,
                provider=provider_name,
                model=model_name,
                status="succeeded",
                cost=cost
            )
            
            log_role_run(role_run.dict())
            log_cost(
                project=inputs.get("project", "unknown"),
                cost_type="llm",
                provider=provider_name,
                tokens_or_items=len(str(result)),
                unit_cost=0.00001,  # 示例单价
                total=cost
            )
            
            logger.info(f"[{role}] 执行成功，成本: ${cost:.4f}")
            return role_run
            
        except Exception as e:
            logger.error(f"[{role}] 执行失败: {e}")
            role_run = RoleRun(
                id=run_id,
                role=role,
                inputs=inputs,
                provider=provider_name,
                model=model_name,
                status="failed",
                error=str(e),
                cost=0.0
            )
            log_role_run(role_run.dict())
            return role_run
    
    def _build_prompt(self, role: str, inputs: Dict[str, Any], 
                     story_bible: Optional[StoryBible]) -> str:
        """
        构建角色提示词（使用模板渲染，加入故事事实库）
        
        Args:
            role: 角色名称
            inputs: 输入参数字典
            story_bible: 故事事实库（可选）
            
        Returns:
            渲染后的提示词字符串
        """
        from storyforge.utils import load_prompt
        
        # 准备上下文
        context = dict(inputs)
        
        # 添加story_bible信息
        if story_bible:
            context['facts'] = '\n'.join(f"- {f}" for f in story_bible.facts) if story_bible.facts else ''
            context['style_guide'] = str(story_bible.styleGuide) if story_bible.styleGuide else ''
        else:
            context['facts'] = ''
            context['style_guide'] = ''
        
        # 加载并渲染提示词模板
        try:
            prompt = load_prompt(role, context)
            return prompt
        except Exception as e:
            logger.warning(f"加载提示词模板失败，使用默认模板: {e}")
            # 回退到简单提示词
            return self._build_fallback_prompt(role, inputs, story_bible)
    
    def _get_role_system_prompt(self, role: str) -> str:
        """
        获取角色的系统提示词
        
        Args:
            role: 角色名称
            
        Returns:
            系统提示词字符串
        """
        prompts = {
            "planner": "你是策划师，负责从题材与爽点抽取主题、节奏曲线与季/集配额。",
            "outliner": "你是大纲师，负责生成分季/分集骨架（beats），对齐爽点与反转节奏。",
            "character_designer": "你是角色设计师，负责输出人物卡与关系网。",
            "scriptwriter": "你是剧本作者，负责按集生成剧本摘要与场景要素（可转分镜）。",
            "editor": "你是编辑与稽核，负责一致性检查、冲突检测与可改编性评审（视频友好）。"
        }
        return prompts.get(role, "你是一个AI助手。")
    
    def _build_fallback_prompt(self, role: str, inputs: Dict[str, Any],
                               story_bible: Optional[StoryBible]) -> str:
        """
        构建后备提示词（当模板加载失败时使用的默认提示词）
        
        Args:
            role: 角色名称
            inputs: 输入参数字典
            story_bible: 故事事实库（可选）
            
        Returns:
            后备提示词字符串
        """
        prompt_parts = []
        
        if story_bible:
            if story_bible.facts:
                prompt_parts.append(f"剧情事实:\n" + "\n".join(f"- {f}" for f in story_bible.facts))
            if story_bible.styleGuide:
                prompt_parts.append(f"风格指南:\n{story_bible.styleGuide}")
        
        if role == "planner":
            prompt_parts.append(f"请根据以下大纲生成分季计划:\n{inputs.get('outline', '')}")
            prompt_parts.append("输出JSON格式: {seasonThemes: [], pacingCurve: {}, episodeQuota: number}")
        elif role == "outliner":
            prompt_parts.append(f"请为第{inputs.get('episodeIndex', 1)}集生成节拍:\n{inputs.get('outline', '')}")
            prompt_parts.append("输出JSON格式: {episodeIndex: number, beats: [{type, hook, durationHint}]}")
        elif role == "character_designer":
            prompt_parts.append(f"请设计人物:\n{inputs.get('description', '')}")
            prompt_parts.append("输出JSON格式: {name, bio, traits: [], goals: [], conflicts: [], visualRefs: []}")
        elif role == "scriptwriter":
            prompt_parts.append(f"请为以下节拍生成场景计划:\n{inputs.get('beats', '')}")
            prompt_parts.append("输出JSON格式: [{sceneIndex, summary, elements: [], dialogue, durationHint}]")
        elif role == "editor":
            prompt_parts.append(f"请评审以下内容:\n{inputs.get('content', '')}")
            prompt_parts.append("输出JSON格式: {target, issues: [], suggestions: [], decision: 'approve'|'revise'}")
        
        return "\n\n".join(prompt_parts)
    
    def _extract_json_from_text(self, text: str) -> Optional[Dict[str, Any]]:
        """
        从文本中提取JSON块（改进版，提取最完整的JSON对象）
        
        Args:
            text: 包含JSON的文本
            
        Returns:
            提取的JSON字典，失败返回None
        """
        import json
        import re
        
        def score_candidate(obj: dict) -> int:
            """根据是否包含大纲关键字段为候选JSON打分"""
            if not isinstance(obj, dict):
                return -10
            outline_keys = {
                "plotStages", "worldView", "mainPlot", "keyConflicts",
                "turningPoints", "themes", "styleGuide", "targetAudience",
                "productionNotes", "sellingPoints", "genre", "targetEpisodes",
                "characters", "title"
            }
            score = 0
            for k in outline_keys:
                if k in obj:
                    # 关键结构字段权重大
                    score += 10 if k in {"plotStages", "worldView", "mainPlot", "characters"} else 5
            # 人物列表结构加分
            chars = obj.get("characters")
            if isinstance(chars, list) and (len(chars) > 0 and isinstance(chars[0], dict)):
                score += 5
            # 仅人物卡特征（无大纲关键字段）则扣分
            if not any(k in obj for k in ["plotStages", "worldView", "mainPlot", "characters"]):
                char_like_keys = {"name", "role", "age", "background", "personality", "abilities", "goals", "conflicts", "characterArc", "relationships"}
                if len(char_like_keys.intersection(obj.keys())) >= 3:
                    score -= 15
            # 适度考虑键数量
            score += min(10, len(obj))
            return score

        candidates: list[dict] = []

        # 尝试1：查找 ```json ... ``` 代码块，收集全部候选
        json_pattern = r'```(?:json)?\s*\n?(.*?)\n?```'
        matches = re.findall(json_pattern, text, re.DOTALL)
        for match in matches:
            try:
                obj = json.loads(match.strip())
                if isinstance(obj, dict):
                    candidates.append(obj)
            except json.JSONDecodeError:
                continue
        
        # 尝试2：使用平衡括号算法提取首个完整JSON对象作为候选
        try:
            start = text.find('{')
            if start != -1:
                depth = 0
                in_string = False
                escape = False
                for i in range(start, len(text)):
                    ch = text[i]
                    if escape:
                        escape = False
                        continue
                    if ch == '\\':
                        escape = True
                        continue
                    if ch == '"' and not in_string:
                        in_string = True
                    elif ch == '"' and in_string:
                        in_string = False
                    elif ch == '{' and not in_string:
                        depth += 1
                    elif ch == '}' and not in_string:
                        depth -= 1
                        if depth == 0:
                            json_str = text[start:i+1]
                            try:
                                obj = json.loads(json_str)
                                if isinstance(obj, dict):
                                    candidates.append(obj)
                            except json.JSONDecodeError:
                                pass
                            break
        except Exception as e:
            logger.debug(f"平衡括号提取失败: {e}")
        
        # 尝试3：简单的正则（可能提取多个），都加入候选
        brace_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
        brace_matches = re.findall(brace_pattern, text, re.DOTALL)
        for match in brace_matches:
            try:
                obj = json.loads(match)
                if isinstance(obj, dict):
                    candidates.append(obj)
            except json.JSONDecodeError:
                continue

        if not candidates:
            return None

        # 尝试将若干候选组装为详细大纲（当顶层对象缺失/被截断时）
        try:
            char_like_keys = {"name", "role", "age", "background", "personality", "abilities", "goals", "conflicts", "characterArc", "relationships"}
            # 人物容器或人物条目
            container_chars = None
            character_items = []
            for c in candidates:
                if isinstance(c.get("characters"), list) and (len(c["characters"]) == 0 or isinstance(c["characters"][0], dict)):
                    container_chars = c["characters"]
                # 单个人物卡
                if len(char_like_keys.intersection(c.keys())) >= 3 and not isinstance(c.get("characters"), list):
                    character_items.append({k: v for k, v in c.items() if k in char_like_keys})

            # 阶段条目（剧情阶段）
            stage_items = []
            for c in candidates:
                if ("stage" in c or "episodes" in c) and ("summary" in c or "keyEvents" in c):
                    stage_items.append(c)

            # worldView/styleGuide 容器
            world_view = None
            style_guide = None
            title = None
            genre = None
            target_episodes = None
            main_plot = None
            key_conflicts = None
            turning_points = None
            themes = None
            production_notes = None
            selling_points = None
            editor_suggestions = []

            for c in candidates:
                if world_view is None and isinstance(c.get("worldView"), dict):
                    world_view = c["worldView"]
                if style_guide is None and isinstance(c.get("styleGuide"), dict):
                    style_guide = c["styleGuide"]
                if title is None and isinstance(c.get("title"), str):
                    title = c["title"]
                if genre is None and isinstance(c.get("genre"), (list, str)):
                    genre = c["genre"]
                if target_episodes is None and isinstance(c.get("targetEpisodes"), int):
                    target_episodes = c["targetEpisodes"]
                if main_plot is None and isinstance(c.get("mainPlot"), str):
                    main_plot = c["mainPlot"]
                if key_conflicts is None and isinstance(c.get("keyConflicts"), list):
                    key_conflicts = c["keyConflicts"]
                if turning_points is None and isinstance(c.get("turningPoints"), list):
                    turning_points = c["turningPoints"]
                if themes is None and isinstance(c.get("themes"), list):
                    themes = c["themes"]
                if production_notes is None and isinstance(c.get("productionNotes"), str):
                    production_notes = c["productionNotes"]
                if selling_points is None and isinstance(c.get("sellingPoints"), list):
                    selling_points = c["sellingPoints"]
                if isinstance(c.get("editorSuggestions"), list):
                    editor_suggestions.extend(c["editorSuggestions"])

            assembled = {}
            # 组装条件：只要出现了阶段或人物任一类信息，即可组装一个最小大纲
            if stage_items or container_chars is not None or character_items:
                if title:
                    assembled["title"] = title
                if genre is not None:
                    assembled["genre"] = genre if isinstance(genre, list) else [genre]
                if target_episodes is not None:
                    assembled["targetEpisodes"] = target_episodes
                if world_view is not None:
                    assembled["worldView"] = world_view
                # 人物优先使用容器，否则用条目集合
                if container_chars is not None:
                    assembled["characters"] = container_chars
                elif character_items:
                    assembled["characters"] = character_items
                if stage_items:
                    # 尽量按 stage 升序
                    try:
                        stage_items_sorted = sorted(stage_items, key=lambda x: x.get("stage", 0))
                    except Exception:
                        stage_items_sorted = stage_items
                    assembled["plotStages"] = stage_items_sorted
                if main_plot is not None:
                    assembled["mainPlot"] = main_plot
                if key_conflicts is not None:
                    assembled["keyConflicts"] = key_conflicts
                if turning_points is not None:
                    assembled["turningPoints"] = turning_points
                if themes is not None:
                    assembled["themes"] = themes
                if style_guide is not None:
                    assembled["styleGuide"] = style_guide
                if production_notes is not None:
                    assembled["productionNotes"] = production_notes
                if selling_points is not None:
                    assembled["sellingPoints"] = selling_points
                if editor_suggestions:
                    assembled["editorSuggestions"] = editor_suggestions

                # 如已包含核心结构，即优先返回组装结果
                if any(k in assembled for k in ["plotStages", "characters", "worldView", "mainPlot"]):
                    return assembled
        except Exception as e:
            logger.debug(f"组装详细大纲失败，回退至评分选择: {e}")

        # 选择得分最高的候选作为结果
        best = max(candidates, key=score_candidate)
        return best
    
    def _estimate_cost(self, provider: str, tokens: int) -> float:
        """
        估算成本（简化实现，实际应根据提供商定价计算）
        
        Args:
            provider: 提供商名称
            tokens: token数量
            
        Returns:
            估算成本（美元）
        """
        # 简化估算：每1000 tokens $0.01
        return tokens / 1000 * 0.01


class MultiRolePipeline:
    """多角色协作流水线"""
    
    def __init__(self):
        """初始化流水线"""
        self.router = RoleRouter()
        self.executor = RoleExecutor(self.router)
    
    def generate_detailed_outline(
        self,
        project_name: str,
        rough_outline: str,
        target_episodes: int = 100,
        story_bible: Optional[StoryBible] = None
    ) -> Dict[str, Any]:
        """
        多角色协作生成详细大纲
        
        多角色模式下，我们仍然主要依靠单个角色（outline_expander）生成核心内容，
        然后由其他角色补充和评审。
        
        Args:
            project_name: 项目名称
            rough_outline: 粗粒度大纲内容
            target_episodes: 目标集数
            story_bible: 故事事实库
            
        Returns:
            详细大纲字典
        """
        logger.info(f"开始多角色协作生成详细大纲（目标集数: {target_episodes}）")
        
        # 创建运行目录
        run_dir = create_multi_role_run_dir(project_name, "outline_generation")
        logger.info(f"多角色运行目录: {run_dir}")
        
        # 保存运行信息
        import time
        from datetime import datetime
        run_info = {
            "project": project_name,
            "run_type": "outline_generation",
            "start_time": datetime.now().isoformat(),
            "target_episodes": target_episodes,
            "rough_outline_length": len(rough_outline)
        }
        save_json(run_info, str(run_dir / "run_info.json"))
        
        step_counter = 1  # 步骤计数器
        
        # 1. 首先使用 outline_expander 生成核心详细大纲（这是主要内容）
        # 注意：这里我们调用 outline_expander 角色，而不是 planner
        expander_inputs = {
            "project": project_name,
            "rough_outline": rough_outline,  # 使用 'rough_outline' 键名
            "target_episodes": target_episodes,  # 使用 'target_episodes' 键名
            "task": "outline_expansion"
        }
        
        # 调用 outline_expander 生成详细大纲
        start_time = time.time()
        expander_run = self._execute_outline_expander(expander_inputs, story_bible, rough_outline)
        elapsed = time.time() - start_time
        
        if expander_run.status != "succeeded":
            raise Exception(f"Outline Expander失败: {expander_run.error}")
        
        # expander 已经生成了完整的详细大纲
        detailed_outline = expander_run.output
        logger.info(f"Outline Expander完成: 详细大纲生成")
        
        # 保存 outline_expander 输出
        save_role_output(
            run_dir=run_dir,
            role_name="outline_expander",
            step_number=step_counter,
            output_data=detailed_outline,
            input_data=expander_inputs,
            metadata={
                "status": expander_run.status,
                "elapsed_time": f"{elapsed:.2f}秒",
                "cost": expander_run.cost
            }
        )
        step_counter += 1
        
        # 2. Character Designer: 补充或优化人物设定（可选）
        if not detailed_outline.get("characters") or len(detailed_outline.get("characters", [])) < 3:
            logger.info("人物设定不足，由 Character Designer 补充...")
            character_inputs = {
                "project": project_name,
                "description": f"根据以下大纲补充人物设定：\n{rough_outline}",
                "outline": rough_outline,
                "outline_plan": detailed_outline.get("plotStages", []),
                "task": "character_design"
            }
            
            start_time = time.time()
            character_run = self.executor.execute_role("character_designer", character_inputs, story_bible)
            elapsed = time.time() - start_time
            
            if character_run.status == "succeeded":
                character_output = character_run.output
                
                # 保存 character_designer 输出
                save_role_output(
                    run_dir=run_dir,
                    role_name="character_designer",
                    step_number=step_counter,
                    output_data=character_output,
                    input_data=character_inputs,
                    metadata={
                        "status": character_run.status,
                        "elapsed_time": f"{elapsed:.2f}秒",
                        "cost": character_run.cost
                    }
                )
                step_counter += 1
                
                # 合并人物设定
                if "characters" in character_output:
                    existing_chars = detailed_outline.get("characters", [])
                    detailed_outline["characters"] = existing_chars + character_output.get("characters", [])
                    logger.info(f"Character Designer完成: 补充人物设定")
        
        # 3. Editor: 评审详细大纲并给出建议（支持1轮回炉）
        import json
        editor_inputs = {
            "project": project_name,
            "target": "outliner",  # 评审目标是详细大纲
            "content": json.dumps(detailed_outline, ensure_ascii=False, indent=2)
        }
        
        start_time = time.time()
        editor_run = self.executor.execute_role("editor", editor_inputs, story_bible)
        elapsed = time.time() - start_time
        
        if editor_run.status == "succeeded":
            review = editor_run.output
            
            # 保存 editor 输出
            save_role_output(
                run_dir=run_dir,
                role_name="editor",
                step_number=step_counter,
                output_data=review,
                input_data={"project": project_name, "target": "outliner"},
                metadata={
                    "status": editor_run.status,
                    "elapsed_time": f"{elapsed:.2f}秒",
                    "cost": editor_run.cost
                }
            )
            step_counter += 1
            
            if isinstance(review, dict):
                if review.get("decision") == "revise":
                    logger.info(f"Editor要求修订，重新生成详细大纲...")
                    
                    # 强制回炉：重新调用 outline_expander，传入修订建议
                    expander_inputs["revision_suggestions"] = review.get("suggestions", [])
                    expander_inputs["revision_issues"] = review.get("issues", [])
                    
                    start_time = time.time()
                    expander_run_revised = self._execute_outline_expander(expander_inputs, story_bible, rough_outline)
                    elapsed = time.time() - start_time
                    
                    if expander_run_revised.status == "succeeded":
                        detailed_outline = expander_run_revised.output
                        logger.info(f"Outline Expander完成修订版本")
                        
                        # 保存修订后的输出
                        save_role_output(
                            run_dir=run_dir,
                            role_name="outline_expander_revised",
                            step_number=step_counter,
                            output_data=detailed_outline,
                            input_data=expander_inputs,
                            metadata={
                                "status": expander_run_revised.status,
                                "elapsed_time": f"{elapsed:.2f}秒",
                                "cost": expander_run_revised.cost,
                                "revision": True
                            }
                        )
                        step_counter += 1
                    else:
                        logger.warning(f"修订失败，使用原版本: {expander_run_revised.error}")
                
                # 记录评审建议
                if "suggestions" in review and review["suggestions"]:
                    detailed_outline["editorSuggestions"] = review["suggestions"]
                logger.info(f"Editor完成: 详细大纲评审")
        
        # 保存最终结果
        save_json(detailed_outline, str(run_dir / "final_result.json"))
        
        # 保存最终结果的Markdown版本
        final_md = self._format_detailed_outline_markdown(detailed_outline)
        save_text(final_md, str(run_dir / "final_result.md"))
        
        # 更新运行信息
        run_info["end_time"] = datetime.now().isoformat()
        run_info["total_steps"] = step_counter - 1
        run_info["status"] = "completed"
        save_json(run_info, str(run_dir / "run_info.json"))
        
        logger.info(f"多角色协作完成，详细大纲已生成")
        logger.info(f"运行结果已保存到: {run_dir}")
        return detailed_outline
    
    def _format_detailed_outline_markdown(self, outline_dict: dict) -> str:
        """
        将详细大纲字典格式化为 Markdown 格式
        
        Args:
            outline_dict: 详细大纲字典
            
        Returns:
            Markdown 格式的字符串
        """
        import json
        
        md_lines = []
        
        # 标题
        if "title" in outline_dict:
            md_lines.append(f"# {outline_dict['title']}\n")
        
        # 基本信息
        md_lines.append("## 基本信息\n")
        if "targetEpisodes" in outline_dict:
            md_lines.append(f"- **目标集数**: {outline_dict['targetEpisodes']}集")
        if "genre" in outline_dict:
            md_lines.append(f"- **题材类型**: {', '.join(outline_dict['genre'])}")
        md_lines.append("")
        
        # 世界观
        if "worldView" in outline_dict:
            md_lines.append("## 世界观设定\n")
            md_lines.append("```json")
            md_lines.append(json.dumps(outline_dict["worldView"], ensure_ascii=False, indent=2))
            md_lines.append("```\n")
        
        # 主要人物
        if "characters" in outline_dict:
            md_lines.append("## 主要人物\n")
            for i, char in enumerate(outline_dict["characters"], 1):
                name = char.get('name', f'人物{i}')
                role = char.get('role', '')
                md_lines.append(f"### {i}. {name} ({role})\n")
                md_lines.append("```json")
                md_lines.append(json.dumps(char, ensure_ascii=False, indent=2))
                md_lines.append("```\n")
        
        # 主线剧情
        if "plotStages" in outline_dict:
            md_lines.append("## 主线剧情\n")
            for i, stage in enumerate(outline_dict["plotStages"], 1):
                title = stage.get('title', f'阶段{i}')
                md_lines.append(f"### {i}. {title}\n")
                if "description" in stage:
                    md_lines.append(stage["description"])
                    md_lines.append("")
        
        # 关键冲突
        if "conflicts" in outline_dict:
            md_lines.append("## 关键冲突\n")
            for i, conflict in enumerate(outline_dict["conflicts"], 1):
                md_lines.append(f"{i}. {conflict}")
            md_lines.append("")
        
        # 编辑建议（如果有）
        if "editorSuggestions" in outline_dict:
            md_lines.append("## 编辑建议\n")
            for i, suggestion in enumerate(outline_dict["editorSuggestions"], 1):
                md_lines.append(f"{i}. {suggestion}")
            md_lines.append("")
        
        return "\n".join(md_lines)
    
    def _execute_outline_expander(
        self,
        inputs: Dict[str, Any],
        story_bible: Optional[StoryBible],
        rough_outline: str
    ) -> RoleRun:
        """
        执行 outline_expander 角色生成详细大纲
        
        Args:
            inputs: 输入参数
            story_bible: 故事事实库
            rough_outline: 粗粒度大纲
            
        Returns:
            RoleRun 对象
        """
        from storyforge.utils import load_prompt
        from storyforge.config import get_config
        
        provider_name, model_name = self.router.get_provider_for_role("planner")  # 复用 planner 的配置
        provider = self.router.create_provider(provider_name)
        
        run_id = generate_id("outline_expander_")
        
        try:
            # 构建提示词（使用 outline_expander 模板）
            config = get_config()
            context = {
                "rough_outline": rough_outline,
                "target_episodes": inputs.get("target_episodes", 100)
            }
            
            # 加载 outline_expander 提示词
            prompt = load_prompt(
                role="outline_expander",
                context=context,
                prompts_dir=config.paths.prompts,
                prompt_set="default"  # 可以根据题材选择 fantasy/romance 等
            )
            
            # 调用LLM生成
            messages = [
                {"role": "system", "content": "你是一位资深的短剧大纲师和故事架构师。"},
                {"role": "user", "content": prompt}
            ]
            
            response_format = {"type": "json_object"}
            llm_config = getattr(config, 'llm', None)
            # 使用配置中的温度，默认为 0.7 (outline)
            temperature = llm_config.temperatures.get("outline", 0.7) if llm_config and hasattr(llm_config, 'temperatures') else 0.7
            
            result = provider.generate(
                messages=messages,
                temperature=temperature,
                response_format=response_format
            )
            
            # 解析结果
            import json
            try:
                output = json.loads(result) if isinstance(result, str) else result
                if not isinstance(output, dict):
                    output = {"result": str(output)}
            except json.JSONDecodeError as e:
                logger.warning(f"[outline_expander] JSON解析失败: {e}")
                output = self.executor._extract_json_from_text(result)
                if not output:
                    raise Exception(f"无法解析JSON输出: {e}")
            
            # 估算成本
            cost = self.executor._estimate_cost(provider_name, len(str(result)))
            
            role_run = RoleRun(
                id=run_id,
                role="planner",  # 记录为 planner 角色
                inputs=inputs,
                output=output,
                provider=provider_name,
                model=model_name,
                status="succeeded",
                cost=cost
            )
            
            log_role_run(role_run.dict())
            log_cost(
                project=inputs.get("project", "unknown"),
                cost_type="llm",
                provider=provider_name,
                tokens_or_items=len(str(result)),
                unit_cost=0.00001,
                total=cost
            )
            
            logger.info(f"[outline_expander] 执行成功，成本: ${cost:.4f}")
            return role_run
            
        except Exception as e:
            logger.error(f"[outline_expander] 执行失败: {e}")
            role_run = RoleRun(
                id=run_id,
                role="planner",
                inputs=inputs,
                provider=provider_name,
                model=model_name,
                status="failed",
                error=str(e),
                cost=0.0
            )
            log_role_run(role_run.dict())
            return role_run
    
    def _merge_outline_outputs(
        self,
        rough_outline: str,
        target_episodes: int,
        planner_output: Dict[str, Any],
        character_output: Dict[str, Any],
        scriptwriter_output: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        合并各角色的输出为完整的详细大纲
        
        Args:
            rough_outline: 粗粒度大纲
            target_episodes: 目标集数
            planner_output: Planner输出
            character_output: Character Designer输出
            scriptwriter_output: Scriptwriter输出
            
        Returns:
            合并后的详细大纲字典
        """
        detailed_outline = {
            "title": planner_output.get("title", "未命名作品"),
            "genre": planner_output.get("genre", ["通用"]),
            "targetEpisodes": target_episodes,
            "worldView": planner_output.get("worldView", {}),
            "characters": character_output.get("characters", []),
            "plotStages": planner_output.get("plotStages", []),
            "mainPlot": planner_output.get("mainPlot", ""),
            "keyConflicts": planner_output.get("keyConflicts", []),
            "turningPoints": planner_output.get("turningPoints", []),
            "themes": planner_output.get("themes", []),
            "styleGuide": {
                "pacing": planner_output.get("pacing", "快节奏"),
                "tone": planner_output.get("tone", ""),
                "visualStyle": scriptwriter_output.get("visualStyle", ""),
                "narrativeStyle": scriptwriter_output.get("narrativeStyle", "")
            },
            "targetAudience": planner_output.get("targetAudience", ""),
            "productionNotes": scriptwriter_output.get("productionNotes", ""),
            "sellingPoints": planner_output.get("sellingPoints", [])
        }
        
        return detailed_outline
    
    def generate_novel(
        self,
        project_name: str,
        detailed_outline: Dict[str, Any],
        target_episodes: Optional[int] = None,
        chapters: Optional[int] = None,
        story_bible: Optional[StoryBible] = None
    ) -> Dict[str, Any]:
        """
        多角色协作生成小说正文
        
        Args:
            project_name: 项目名称
            detailed_outline: 详细大纲
            target_episodes: 参考目标集数
            chapters: 期望章节数
            story_bible: 故事事实库
            
        Returns:
            小说正文字典 {title, chapters: [{title, summary, content}], wordCount}
        """
        logger.info(f"开始多角色协作生成小说正文（章节数: {chapters or '自动'}）")
        
        # 创建多角色运行目录用于保存中间结果
        run_dir = create_multi_role_run_dir(project_name, "novel_generation")
        logger.info(f"多角色运行目录: {run_dir}")
        
        # 1. Planner: 规划章节结构与节奏分配
        planner_inputs = {
            "project": project_name,
            "detailed_outline": detailed_outline,
            "target_episodes": target_episodes or 0,
            "chapters": chapters or 0,
            "task": "chapter_planning"
        }
        planner_run = self.executor.execute_role("planner", planner_inputs, story_bible)
        
        # 保存 planner 输出
        save_role_output(run_dir, "planner", 1, planner_run.output)
        
        if planner_run.status != "succeeded":
            raise Exception(f"Planner失败: {planner_run.error}")
        
        chapter_plan = planner_run.output
        actual_chapters = chapters or chapter_plan.get("suggestedChapters", 100)
        logger.info(f"Planner完成: 章节结构规划（{actual_chapters}章）")
        
        # 2. Character Designer: 细化人物性格与对话风格
        character_inputs = {
            "project": project_name,
            "detailed_outline": detailed_outline,
            "task": "character_refinement"
        }
        character_run = self.executor.execute_role("character_designer", character_inputs, story_bible)
        
        # 保存 character designer 输出
        if character_run.status == "succeeded":
            save_role_output(run_dir, "character_designer", 2, character_run.output)
        
        if character_run.status != "succeeded":
            logger.warning(f"Character Designer失败: {character_run.error}")
            character_details = {}
        else:
            character_details = character_run.output
            logger.info(f"Character Designer完成: 人物细化")
        
        # 3. Scriptwriter: 撰写章节正文（分批处理）
        all_chapters = []
        batch_size = 10  # 每批处理10章
        
        for batch_start in range(1, actual_chapters + 1, batch_size):
            batch_end = min(batch_start + batch_size - 1, actual_chapters)
            
            scriptwriter_inputs = {
                "project": project_name,
                "detailed_outline": detailed_outline,
                "chapter_plan": chapter_plan,
                "character_details": character_details,
                "chapter_range": f"{batch_start}-{batch_end}",
                "task": "novel_writing"
            }
            scriptwriter_run = self.executor.execute_role("scriptwriter", scriptwriter_inputs, story_bible)
            
            # 保存 scriptwriter 输出（步骤号从3开始，每批递增）
            batch_step_number = 3 + (batch_start - 1) // batch_size
            if scriptwriter_run.status == "succeeded":
                save_role_output(run_dir, f"scriptwriter_ch{batch_start:03d}-{batch_end:03d}", batch_step_number, scriptwriter_run.output)
            else:
                # 即使失败也保存错误信息
                error_data = {
                    "status": "failed",
                    "error": scriptwriter_run.error,
                    "raw_output": str(scriptwriter_run.output) if scriptwriter_run.output else None
                }
                save_role_output(run_dir, f"scriptwriter_ch{batch_start:03d}-{batch_end:03d}_error", batch_step_number, error_data)
            
            if scriptwriter_run.status != "succeeded":
                logger.warning(f"Scriptwriter失败 (第{batch_start}-{batch_end}章): {scriptwriter_run.error}")
                continue
            
            # 规范化脚本作者输出为 chapters 结构
            out = scriptwriter_run.output or {}
            batch_chapters = out.get("chapters", [])

            # 定义转换函数
            def scene_to_chapter(scene_obj: Dict[str, Any], index: int) -> Dict[str, Any]:
                summary = scene_obj.get("summary", "")
                elements = scene_obj.get("elements") or scene_obj.get("keyElements") or []
                dialogue = scene_obj.get("dialogue") or scene_obj.get("dialogues") or ""
                duration = scene_obj.get("durationHint") or scene_obj.get("duration") or ""

                parts: List[str] = []
                if summary:
                    parts.append(summary)
                if elements and isinstance(elements, list):
                    parts.append(f"要素：{', '.join(str(e) for e in elements)}")
                if dialogue:
                    parts.append(f"对白：{dialogue}")
                if duration:
                    parts.append(f"时长提示：{duration}")

                content = "\n\n".join(parts)
                return {
                    "title": f"第{index}章",
                    "summary": summary,
                    "content": content
                }

            # 检查 batch_chapters 是否有效（包含 content）
            is_valid_chapters = False
            if batch_chapters and isinstance(batch_chapters, list):
                # 检查第一个元素是否有 content
                if len(batch_chapters) > 0 and isinstance(batch_chapters[0], dict) and batch_chapters[0].get("content"):
                    is_valid_chapters = True
            
            # 若 chapters 无效（为空或格式不对），尝试从 scenes 转换
            if not is_valid_chapters:
                scenes = out.get("scenes") or out.get("items") or []
                # 如果 batch_chapters 有值但无效（可能是被重命名过来的 scenes），也当作 scenes 处理
                if not scenes and batch_chapters and not is_valid_chapters:
                    scenes = batch_chapters
                
                if isinstance(scenes, list) and scenes:
                    batch_chapters = [scene_to_chapter(s, batch_start + i) for i, s in enumerate(scenes)]

            all_chapters.extend(batch_chapters)
            logger.info(f"Scriptwriter完成第{batch_start}-{batch_end}章")
        
        # 4. 汇聚：组装小说
        novel = {
            "title": detailed_outline.get("title", "未命名小说"),
            "chapters": all_chapters,
            "wordCount": sum(len(ch.get("content", "")) for ch in all_chapters)
        }
        
        # 5. Editor: 评审（可选，对前几章和后几章抽样检查）
        sample_chapters = all_chapters[:3] + all_chapters[-3:] if len(all_chapters) > 6 else all_chapters
        editor_inputs = {
            "project": project_name,
            "target": "novel",
            "content": str(sample_chapters),
            "full_chapter_count": len(all_chapters)
        }
        editor_run = self.executor.execute_role("editor", editor_inputs, story_bible)
        
        # 保存 editor 输出
        editor_step_number = 3 + ((actual_chapters - 1) // batch_size) + 1
        if editor_run.status == "succeeded":
            save_role_output(run_dir, "editor", editor_step_number, editor_run.output)
        
        if editor_run.status == "succeeded":
            review = editor_run.output
            if isinstance(review, dict) and review.get("decision") == "revise":
                logger.info(f"Editor建议修订（建议：{review.get('suggestions', [])}）")
                # 简化处理：记录建议但不重写
                novel["editorSuggestions"] = review.get("suggestions", [])
        
        # 保存最终结果
        final_novel_path = run_dir / "final_novel.json"
        save_json(novel, str(final_novel_path))
        
        logger.info(f"多角色协作完成，小说正文已生成（{len(all_chapters)}章，{novel['wordCount']}字）")
        logger.info(f"运行结果已保存到: {run_dir}")
        return novel
    
    def generate_episodes(
        self,
        project_name: str,
        outline: str,
        count: int,
        story_bible: Optional[StoryBible] = None
    ) -> List[EpisodeBeats]:
        """
        生成分集（多角色协作）
        
        Args:
            project_name: 项目名称
            outline: 大纲
            count: 剧集数量
            story_bible: 故事事实库
            
        Returns:
            EpisodeBeats列表
        """
        logger.info(f"开始多角色协作生成 {count} 集")
        
        # 1. Planner: 生成分季计划
        planner_inputs = {
            "project": project_name,
            "outline": outline,
            "count": count
        }
        planner_run = self.executor.execute_role("planner", planner_inputs, story_bible)
        
        if planner_run.status != "succeeded":
            raise Exception(f"Planner失败: {planner_run.error}")
        
        outline_plan = OutlinePlan(**planner_run.output)
        logger.info(f"Planner完成: {outline_plan.episodeQuota}集配额")
        
        # 2. Outliner: 为每集生成节拍（支持最多2轮修订）
        episodes = []
        max_iterations = 2  # 最多2轮修订
        
        for i in range(1, count + 1):
            outliner_inputs = {
                "project": project_name,
                "episodeIndex": i,
                "outline": outline
            }
            outliner_run = self.executor.execute_role("outliner", outliner_inputs, story_bible)
            
            if outliner_run.status != "succeeded":
                logger.warning(f"Outliner失败 (第{i}集): {outliner_run.error}")
                continue
            
            # 3. Editor: 评审（支持最多2轮修订）
            for iteration in range(max_iterations):
                editor_inputs = {
                    "project": project_name,
                    "target": "outliner",
                    "content": str(outliner_run.output)
                }
                editor_run = self.executor.execute_role("editor", editor_inputs, story_bible)
                
                if editor_run.status == "succeeded":
                    try:
                        review = ReviewReport(**editor_run.output)
                        
                        if review.decision == "approve":
                            logger.info(f"第{i}集通过评审（第{iteration+1}轮）")
                            break  # 通过评审，退出循环
                        
                        elif review.decision == "revise" and len(review.issues) > 0:
                            if iteration < max_iterations - 1:
                                # 还有修订轮次
                                logger.info(f"Editor要求修订第{i}集（第{iteration+1}轮），重新生成...")
                                outliner_inputs["revision"] = review.suggestions
                                outliner_inputs["revision_issues"] = review.issues
                                outliner_run = self.executor.execute_role("outliner", outliner_inputs, story_bible)
                                
                                if outliner_run.status != "succeeded":
                                    logger.warning(f"第{i}集修订失败: {outliner_run.error}")
                                    break
                            else:
                                # 达到最大修订轮次
                                logger.warning(f"第{i}集达到最大修订轮次({max_iterations})，使用当前版本")
                                break
                        else:
                            # decision 为其他值或无 issues
                            break
                    
                    except Exception as e:
                        logger.warning(f"解析 ReviewReport 失败: {e}，跳过评审")
                        break
                else:
                    logger.warning(f"Editor评审失败 (第{i}集): {editor_run.error}")
                    break
            
            if outliner_run.status == "succeeded":
                # 确保 episodeIndex 与当前循环索引一致，避免LLM输出固定为1导致所有结果写入同一集
                try:
                    episode_data = dict(outliner_run.output or {})
                    episode_data["episodeIndex"] = i
                    episodes.append(EpisodeBeats(**episode_data))
                    logger.info(f"第{i}集生成完成")
                except Exception as e:
                    logger.error(f"构造 EpisodeBeats 失败 (第{i}集): {e}")
        
        logger.info(f"多角色协作完成，成功生成 {len(episodes)} 集")
        return episodes
